package com.aconcagua.factory {
  import com.aconcagua.beans.BeanDefinition;
  import com.aconcagua.beans.BeanDefinitionException;
  import com.aconcagua.beans.FactoryBean;
  
  import flash.events.EventDispatcher;
  import flash.utils.Dictionary;
  import flash.utils.getQualifiedClassName;

  //TODO: Implement nested bean definitions
  //TODO: Factory methods
  //TODO: Implement hierarchical factories
  //TODO: Implement post-processors
  public class AbstractBeanFactory extends EventDispatcher implements BeanFactory {
    private var _definitions: Dictionary = new Dictionary();
    private var _anonymousDefinitionsCounter: int = 0;
    private var _singletons: Dictionary = new Dictionary();
    private var _aliases: Dictionary = new Dictionary();
    
    /** 
    * Holds a refererence of the beans that are been instantiated. 
    * Required to detect cross-references deadlocks
    **/
    private var _instantiationChain: Dictionary = new Dictionary();
    private var _instantiationChainLength: int = 0;
    private var _deferredPropertiesExecutions: Array = new Array(); //Array of functions
    
    private function getRegisteredSingleton(name: String): * {
      return _singletons[name];
    }

    public function getBean(name: String):* {
      var bean: * = this.resolveBean(name);
      
      return bean is FactoryBean? FactoryBean(bean).create() : bean;
    }

    protected function resolveBean(id: String): * {
      var beanDefinition: BeanDefinition = this.getBeanDefinition(id);
      
      if (beanDefinition.isSingleton) {
        if (!this.isInitializedSingleton(beanDefinition.name)) {
          //It has not been registered yet (it's lazy),
          //so we register it.
          this.initializeDefinition(beanDefinition, true);
        }
        
        return this.getRegisteredSingleton(beanDefinition.name);
      }
      else /* it's prototype */{
        return this.createPrototype(beanDefinition);
      }
    }
    
    private function createPrototype(beanDefinition: BeanDefinition): * {
      var beanInstance: Object = this.createInstance(beanDefinition);
      this.processProperties(beanDefinition, beanInstance);
      
      return beanInstance;
    }
    
    protected function createInstance(definition: BeanDefinition): Object {
      if (_instantiationChain[definition.name] != null) {
        throw new BeanCurrentlyInCreationException(definition.name);
      }

      try {
        _instantiationChain[definition.name] = definition;
        _instantiationChainLength++;

        var instance: Object = definition.createInstance(this);

        delete _instantiationChain[definition.name];
        _instantiationChainLength--;

        return instance;
      }
      catch(e: Error) {
        throw new BeanCreationException(definition.name, e);
      }

      //This should never execute. Either we return an instance inside the try block,
      //or an exception is thrown from within the catch block.
      //However, the compiler complains if we don't return anything at this point. 
      throw new BeanCreationException(
        definition.name, 
        new Error("Unexpected execution flow")
      );
    }
    
    private function processProperties(definition: BeanDefinition, beanInstance: Object): void {
      this.deferPropertiesExecution(definition, beanInstance);
      
      if (!this.instantiatingNestedBean()) {
        for each(var command: Function in _deferredPropertiesExecutions) {
          command.call(this);
        }
      }
    }

    private function deferPropertiesExecution(definition: BeanDefinition, beanInstance: Object): void {
      _deferredPropertiesExecutions.push(
        function(): void { 
          this.applyProperties(definition, beanInstance);
        }
      );
    }

    protected function applyProperties(definition: BeanDefinition, beanInstance: Object): void {
      definition.applyProperties(beanInstance, this); 

      if (beanInstance is BeanFactoryAware) {
        BeanFactoryAware(beanInstance).beanFactory = this;
      }
    }

    //TODO: REVIEW
    private function instantiatingNestedBean(): Boolean {
      return _instantiationChainLength > 0;
    }

    private function isInitializedSingleton(id: String): Boolean {
      return _singletons[id] != null;
    }

    public function containsBean(id: String): Boolean {
      return this.containsDefinition(id);
    }

    private function containsDefinition(name: String): Boolean {
      return _definitions[name] != null;
    }

    /** 
    * Adds the given singleton instance to the singleton registry. 
    * The instance may have not being fully initialized yet 
    **/
    private function registerSingleton(id: String, beanInstance: Object): void {
      _singletons[id] = beanInstance;
    }
    
    public function registerBeanDefinition(bean: BeanDefinition): void {
      this.validateDefinition(bean);
      if (bean.name == null) {
        bean.name = this.generateNameForDefinition(bean);
      }

      this.addDefinition(bean.name, bean);
      this.initializeDefinition(bean, false);
    }

    public function registerAlias(alias: String, bean: BeanDefinition): void {
      this.addDefinition(alias, bean);
      
      var aliasesForBean: Array = _aliases[bean.name] as Array;
      if (aliasesForBean == null) {
        _aliases[bean.name] = new Array();
        aliasesForBean = _aliases[bean.name] as Array;
      }

      aliasesForBean.push(alias);
    }

    private function validateDefinition(definition: BeanDefinition): void {
      if (this.containsDefinition(definition.name))
        throw new BeanDefinitionException(definition.name, BeanDefinitionException.DUPLICATE_NAME);     
      if (definition.type == null)
        throw new BeanDefinitionException(definition.name, BeanDefinitionException.NO_TYPE);
    }

    private function initializeDefinition(bean: BeanDefinition, initializeLazy: Boolean): void {
      //Instantiate and register singleton instances if they are eager,
      //or if we are forced to initialize lazy definitions.
      //Prototype beans will not be initialized, since instances will be created on demand.
      if (bean.isSingleton && (!bean.lazy || initializeLazy)) {
        var beanInstance:Object = this.createInstance(bean);
        this.registerSingleton(bean.name, beanInstance);
        this.processProperties(bean, beanInstance);
      }
    }
    
    protected function getBeanDefinition(name: String): BeanDefinition {
      var beanDefinition: BeanDefinition = _definitions[name];
      if (beanDefinition == null) {
        throw new NoSuchBeanDefinitionException("Undefined bean: " + name);
      }
      
      return beanDefinition;
    }

    private function addDefinition(name: String, bean: BeanDefinition): void {
      if (this.containsBean(name)) {
        throw new BeanDefinitionException(
          name, 
          BeanDefinitionException.DUPLICATE_NAME
        );     
      }

      _definitions[name] = bean;
    }
    
    public function getAliases(name: String): Array {
      return _aliases[name] as Array != null? _aliases[name] : new Array();
    }
    
    public function getType(name: String): Class {
      return this.getBeanDefinition(name).type;
    }
    
    public function isPrototype(name: String): Boolean {
      return this.getBeanDefinition(name).isPrototype; 
    }
    
    public function isSingleton(name: String): Boolean {
      return this.getBeanDefinition(name).isSingleton;
    }
    
    public function isTypeMatch(name: String, targetType: Class): Boolean {
      return this.getBeanDefinition(name).type == targetType;
    }
    
    private function generateNameForDefinition(definition: BeanDefinition): String {
      var name: String = "anonymous-bean:" + _anonymousDefinitionsCounter++  + ":" + getQualifiedClassName(definition.type);
      return name;
    }
  }
}